﻿/* ----------------------------------------------------------------

	This file is part of TCP/IP Message Manager
	
	Copyright (C) 2014 Aleksejs Belezjaks http://alexbelezjaks.com
	
	TCP/IP Message Manager is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    TCP/IP Message Manager is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
	--------------------------------------------------------------- */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace MsgManager
{
    /// <summary>
    /// Client is created and stored when new Thread is started to listen to client.
    /// Mainly used to detect and stop specific client listening thread for StopListening methid.
    /// Pretty hacky and ugly.
    /// Made private so can't be seen outside.
    /// </summary>
    class ClientThread
    {
        private Thread thread;
        public Thread Thread
        {
            get { return thread; }
        }

        private Client client;
        public Client Client
        {
            get { return client; }
        }

        // Hacky variable to stop client thread. As bool is atomic we does not need a lock when changing it.
        private bool stop;
        public bool Stop
        {
            get { return stop; }
        }

        public ClientThread(Thread thread, Client client)
        {
            this.thread = thread;
            this.client = client;
            this.stop = false;
        }

        /// <summary>
        /// Sets bool to stop the thread. Should be atomic
        /// Hacky as hell
        /// </summary>
        public void StopThread()
        {
            stop = true;
        }
    }

    /// <summary>
    /// Main MessageManager class. To be used in other program instance of it should be created.
    /// </summary>
    public class MessageManager
    {
        private static readonly TimeSpan PING_CLIENT_INTERVAL = new TimeSpan(0, 0, 1);  // How often do manager tries to ping all active connections. default 1 min
        private Timer pingClients;

        // Name of this istance of the class. Used only pinging aestethics currently.
        private string name;
        // 4 Magic bytes which tells as the message starts here
        private static readonly byte[] magic_number = { 0, 254, 237, 0 };
        // Megabyte, maximum size of the message
        private const uint MAX_BUFFER = 1048576;
        
        private bool stopServer;
        private Thread mainServerThread;       
        private List<ClientThread> clientThreads;

        public bool Running
        {
            get { return !stopServer; }
        }

        public List<Client> ListenedClients
        {
            get
            {
                List<Client> temp = new List<Client>();
                foreach (ClientThread client in clientThreads)
                {
                    temp.Add(client.Client);
                }
                return temp;
            }
        }

        public event EventHandler<MessageReceivedArgs> MessageReceived;
        public event EventHandler<MessageSendResultArgs> MessageSendFailed;
        public event EventHandler<MessageSendResultArgs> MessageSendSuccess;
        public event EventHandler<ClientConnectedArgs> ClientConneted;
        public event EventHandler<ServerConnectedArgs> ServerConnectedSuccess;
        public event EventHandler<ServerConnectedArgs> ServerConnectedFailed;
        public event EventHandler<ClientConnectedArgs> ClientDisconnected;

        public MessageManager(string name)
        {
            this.name = name;

            stopServer = true;
            clientThreads = new List<ClientThread>();

            pingClients = new Timer(PingClients, null, new TimeSpan(0, 0, 10), PING_CLIENT_INTERVAL);
        }

//--------------------------------------- PRIVATE LISTENERS ---------------------------------//

        /// <summary>
        /// Starts server on specified port. When client is connected ClienConnect event is called.
        /// </summary>
        /// <param name="port">port to listen to</param>
        private void ServerLoop(object port)
        {
            // Create server
            TcpListener listener = new TcpListener(IPAddress.Any, (int)port);
            //TcpListener listener = new TcpListener((int)port);    // Deprecated

            listener.Start();
            while (!stopServer)
            {
                // Wait until we have pending client and only then accept it
                // Prevents code locking
                if (listener.Pending())
                {
                    TcpClient client = listener.AcceptTcpClient();  // Could stuck at this line as operation is synchronous

                    ClientConneted(this, new ClientConnectedArgs(new Client(client, client.GetStream())));
                }
                else
                {
                    Thread.Sleep(100);
                }
            }
            listener.Stop();
        }

        /// <summary>
        /// Scans buffer for known message pattern. Could detect multiple messages in one buffer.
        /// </summary>
        /// <param name="buffer">buffer to scan</param>
        /// <param name="find">bytes message should start with</param>
        /// <returns>returns list of indecies where message starts</returns>
        private List<int> ScanBuffer(List<byte> buffer, byte[] find)
        {
            List<int> result = new List<int>();

            // Try to find Magic Number in the buffer
            int len = find.Length;
            byte[] lastFourBytes = new byte[find.Length];

            for (int i = find.Length - 1; i < buffer.Count; i++)
            {
                // Copy last four bytes
                buffer.CopyTo(i - (len - 1), lastFourBytes, 0, len);

                // Compare two byte arrays
                if (CompareArrays(find, lastFourBytes))
                {
                    if (i < buffer.Count) result.Add(i + 1);
                }
            }

            return result;
        }

        /// <summary>
        /// Compare two arrays together.
        /// </summary>
        /// <param name="ar1">first array</param>
        /// <param name="ar2">second array</param>
        /// <returns>return true if two arrays are indentical</returns>
        private bool CompareArrays(byte[] ar1, byte[] ar2)
        {
            if (ar1.Length != ar2.Length) return false;

            for (int i = 1; i < ar1.Length; i++)
            {
                if (ar1[i] != ar2[i]) return false;
            }

            return true;
        }

        /// <summary>
        /// Send message to client. Adding magic before it.
        /// Calls MessageSendSuccess if message send successfully or
        /// MessageSendFailed if any exception is thrown.
        /// </summary>
        /// <param name="obj"></param>
        private void SendMessage(object obj)
        {
            SendMessageThreadArgs args = (SendMessageThreadArgs)obj;
            try
            {
                // Copy message bytes and magic number bytes into single array.
                NetworkStream stream = args.Client.Stream;
                byte[] message = args.Message.Bytes;
                byte[] buffer = new byte[magic_number.Length + message.Length];
                Array.Copy(magic_number, 0, buffer, 0, magic_number.Length);
                Array.Copy(message, 0, buffer, magic_number.Length, message.Length);

                // Lock stream to be sure it is not used to read or write bytes by other Send/Receive methods.
                // It is also important to send bytes in one go,
                // as I noticed sometimes message could be torn into several message and arrive separately.
                Monitor.Enter(stream);
                stream.Write(buffer, 0, buffer.Length);
                stream.Flush();
                Monitor.Exit(stream);

                MessageSendSuccess(this, new MessageSendResultArgs(args.Message, args.Client, null, true));
            }
            catch (Exception ex)
            {
                MessageSendFailed(this, new MessageSendResultArgs(args.Message, args.Client, ex, false));
            }
        }

        /// <summary>
        /// Processes
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private List<Message> ProcessMessage(List<byte> bytes)
        {
            List<Message> result = new List<Message>();
            if (bytes.Count == 0) return result;

            List<int> starts = ScanBuffer(bytes, magic_number);
            for (int i = 0; i < starts.Count; i++)
            {
                if (starts[i] >= bytes.Count) continue;

                int start = starts[i];
                int finish, len;
                if (i == starts.Count - 1)
                {
                    finish = bytes.Count - 1;
                    len = (finish - start) + 1;
                }
                else
                {
                    finish = starts[i + 1];
                    len = (finish - start) - 4;
                }

                byte[] message = new byte[len];
                bytes.CopyTo(start, message, 0, len);

                try
                {
                    Message msg = new Message(message);
                    result.Add(msg);
                }
                catch (Exception ex)
                {
                    Helper.Log(ex.Message);
                }
            }

            return result;
        }

        /// <summary>
        /// Loop to listen for incoming message from client/server
        /// </summary>
        /// <param name="client_thread_obj"></param>
        private void ListenToClient(object client_thread_obj)
        {
            ClientThread clientThread = (ClientThread)client_thread_obj;
            Client client = clientThread.Client;
            List<Message> result = new List<Message>();

            while (!clientThread.Stop)
            {
                result = GetMessage(client);
                foreach (Message message in result)
                {
                    MessageReceived(this, new MessageReceivedArgs(message, client));
                }

                Thread.Sleep(100);
            }
        }

        /// <summary>
        /// Tries to read and process message from client
        /// </summary>
        /// <param name="client">client to read messages from</param>
        /// <returns>return list of received messages</returns>
        private List<Message> GetMessage(Client client)
        {
            NetworkStream stream = client.Stream;
            try
            {
                // Create byte buffer for reading
                List<byte> buffer = new List<byte>();

                Monitor.Enter(stream);
                // Listen for incoming messages, but only if data is available
                while (stream.DataAvailable)
                {
                    // Read next byte
                    buffer.Add((byte)stream.ReadByte());

                    // Make sure buffer is not overflowed
                    if (buffer.Count >= MAX_BUFFER) buffer.Remove(buffer[0]);
                }
                Monitor.Exit(stream);

                return ProcessMessage(buffer);
            }
            catch (IOException ex)
            {
                Helper.Log("Can't read bytes from client");
                return new List<Message>();
            }
        }

        /// <summary>
        /// Ping client/server. If we can't send message when client/server is disconnected, call ClientDisconnected event.
        /// </summary>
        /// <param name="stateInfo"></param>
        private void PingClient(Object stateInfo)
        {
            // NOT IN USE

            Client client = (Client)stateInfo;
            try
            {               
                NetworkStream stream = client.Stream;

                Message message = new Message("ping", "ping by " + name);

                byte[] ping = message.Bytes;
                Monitor.Enter(stream);
                stream.Write(ping, 0, ping.Length);
                stream.Flush();
                Monitor.Exit(stream);
            }
            catch (Exception ex)
            {
                Helper.Log("Ping failed");
                if (ClientDisconnected != null)
                {
                    ClientDisconnected(this, new ClientConnectedArgs(client));
                }
            }
        }

        private void ConnectToServerThread(object target_obj)
        {
            IPEndPoint target = (IPEndPoint)target_obj;
            try
            {
                TcpClient tcp_client = new TcpClient();
                tcp_client.Connect(target);
                NetworkStream clientStream = tcp_client.GetStream();

                ServerConnectedSuccess(this, new ServerConnectedArgs(new Client(tcp_client, clientStream), target));
            }
            catch (Exception ex)
            {
                ServerConnectedFailed(this, new ServerConnectedArgs(null, target));
            }
        }

        private void PingClients(Object stateInfo)
        {
            // Make threadsafe copy.
            Monitor.Enter(clientThreads);
            List<ClientThread> clientThreads_copy = new List<ClientThread>(clientThreads);
            Monitor.Exit(clientThreads);

            foreach (ClientThread thread in clientThreads_copy)
            {
                // Should really try to write bytes, not use existing send message method,
                // So we can lauch ClietnDisconncted event, not MessageSendFailed.
                //SendMessageAsync(thread.Client, new Message("ping", "ping by " + name));

                Thread ping = new Thread(PingClient);
                ping.Start(thread.Client);
            }
        }

//--------------------------------------- PUBLIC METHODS -----------------------------------//

        /// <summary>
        /// Starts server on specified port. When client is connected ClienConnect event is called.
        /// </summary>
        /// <param name="port">port to listen to</param>
        public void StartServer(int port)
        {
            // If server is running no need to start it again
            if (Running) return;

            stopServer = false;
            mainServerThread = new Thread(ServerLoop);
            mainServerThread.Start((object)port);
        }

        /// <summary>
        /// Stops server if it running
        /// Also stop listening to all connected clients
        /// </summary>
        public void StopServer()
        {
            // if (mainServerThread != null) mainServerThread.Abort();
            if (!Running) return;

            stopServer = true;
            StopAllListening();
        }

        /// <summary>
        /// Sends message to specified client synchronously. 
        /// It is better to use SendMessageAsync unless you specifically need it synchronously 
        /// </summary>
        /// <param name="client">client to send message to</param>
        /// <param name="message">message to send</param>
        public void SendMessage(Client client, Message message)
        {
            SendMessage(new SendMessageThreadArgs(client, message));
        }

        /// <summary>
        /// Sends message to specified client asynchronously. 
        /// </summary>
        /// <param name="client">client to send message to</param>
        /// <param name="message">message to send</param>
        public void SendMessageAsync(Client client, Message message)
        {
            Thread sendMessageThread = new Thread(SendMessage);
            sendMessageThread.Start(new SendMessageThreadArgs(client, message));
        }

        /// <summary>
        /// Returns local IP. Does not really guaranteed to return correct one.
        /// Returns 127.0.0.1 if can't find anything.
        /// </summary>
        public static IPAddress LocalIP
        {
            get
            {
                IPHostEntry host;
                IPAddress result = IPAddress.Parse("127.0.0.1");
                host = Dns.GetHostEntry(Dns.GetHostName());
                foreach (IPAddress ip in host.AddressList)
                {
                    if (ip.AddressFamily.ToString() == "InterNetwork")
                    {
                        result = ip;
                    }
                }
                return result;
            }
        }

        /// <summary>
        /// Start listen to messages from client/server.
        /// Could be used for both client and server.
        /// </summary>
        /// <param name="client">client/server to listen to</param>
        /// <returns>returns true if started listening successfully</returns>
        public bool StartListening(Client client)
        {
            // Check if we are already listening for this client
            foreach (ClientThread thread in clientThreads)
            {
                if (thread.Client == client) return false;
            }

            try
            {
                ClientThread clientThread = new ClientThread(new Thread(ListenToClient), client);

                Monitor.Enter(clientThreads);
                {
                    clientThreads.Add(clientThread);
                }
                Monitor.Exit(clientThreads);

                clientThread.Thread.Start(clientThread);

                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        /// <summary>
        /// Stop listen for messages from client/server.
        /// </summary>
        /// <param name="client">client/server to stop listen to</param>
        /// <returns>returns true if stopped listening successfully</returns>
        public bool StopListening(Client client)
        {
            ClientThread found = null;

            // Find ClientThread we should stop using client object
            foreach (ClientThread thread in clientThreads)
            {
                if (thread.Client == client)
                {
                    found = thread;
                    break;
                }
            }

            if (found != null)
            {
                // If found client thread tell it to stop
                Monitor.Enter(clientThreads);
                {
                    found.StopThread();
                    clientThreads.Remove(found);                    
                }
                Monitor.Exit(clientThreads);
                return true;
            }
            else return false;
        }

        /// <summary>
        /// Stop to listen to all clients/servers
        /// </summary>
        public void StopAllListening()
        {
            foreach (ClientThread thread in clientThreads)
            {
                Monitor.Enter(clientThreads);
                {
                    thread.StopThread();
                }
                Monitor.Exit(clientThreads);
            }

            clientThreads.Clear();
        }

        /// <summary>
        /// Tries to connect to server synchronously. When done calls ServerConnectedSuccess or ServerConnectedFailed.
        /// It is better to use ConnectToServerAsync unless you specifically need it synchronously.
        /// </summary>
        /// <param name="target">target server EndPoint</param>
        public void ConnectToServer(IPEndPoint target)
        {
            ConnectToServerThread(target);
        }

        /// <summary>
        /// Tries to connect to server asynchronously. When done calls ServerConnectedSuccess or ServerConnectedFailed.
        /// </summary>
        /// <param name="target">target server EndPoint</param>
        public void ConnectToServerAsync(IPEndPoint target)
        {
            Thread connectToServerThread = new Thread(ConnectToServerThread);
            connectToServerThread.Start(target);
        }

    }
}
